
#import "mCD_Controller.h"
#import "PrefsController.h"
#import "CD_DBase.subproj/CD_DBase.h"

void fixCatNum(struct rsc_media_catnum_reply *);

@implementation mCD_Controller

/* implement a periodic update */
void Trigger(DPSTimedEntry teNum, double new, id target)
{
    [target EventLoop];
}

- EventLoopInit
{
    teNum = DPSAddTimedEntry(updatePeriod, (DPSTimedEntryProc)Trigger,
    				self, NX_BASETHRESHOLD);
    return self;
}

- EventLoop
{
    int  devReady;
    struct timeval	timed_cd_Tval;

    if ( (cd_fd != 0) && !do_timed_updates ) {
	devReady = do_testunitready(cd_fd, &timed_cd_Tval, &tur_Ereply);
	if ( !devReady ) {
	    /* Hmm, will this do what I want? */
	    [self loadCD:self];
	    }
	}

    if ( do_timed_updates ) [self updateCdStatus:self];
    return self;
}

- appDidInit:sender
{
    if ( 0 != geteuid() ) {
	id	nonRootErrorPanel;
	nonRootErrorPanel = [NXApp loadNibSection:"NonRoot.nib" owner:NXApp];
	[NXApp runModalFor:nonRootErrorPanel];
	exit(0);
	}

    [globPrefs initGlobalPreferences:self mainPanel:mainPanel ];

    /* NOTE: in NS-3.2, it seems that setAltIncrementValue will work
     *       backwards, at least for vertical sliders.  I still want
     *       to have it though, it's better backwards than not at all
     */
    [leftVolumeSliderID  setAltIncrementValue: 1.0];
    [rightVolumeSliderID setAltIncrementValue: 1.0];
    [pauseButtonID setShowsStateBy:NX_CONTENTS | NX_CHANGEBACKGROUND];
    
    cd_fd = 0;
    [unloadButtonID setEnabled:NO];

    [curTrackID setStringValue: "--"];
    [endTrackID setStringValue: "--"];
    [trackPlayTimeID setStringValue: "-:--:--"];
    [trackRemTimeID  setStringValue: "-:--:--"];
    [discPlayTimeID setStringValue: "-:--:--"];
    [discRemTimeID  setStringValue: "-:--:--"];
    [trackInfoID selectAll:self];
    [trackInfoID replaceSel: ""];
    [mainPanel makeKeyAndOrderFront:self];

    /* start up the periodic update of the cd status */
    do_timed_updates = NO;	/* initial testing */
    updatePeriod = 1.0;		/* every 1 second */
    updatePeriod = 0.5;		/* crazy times for testing purposes */
    updatePeriod = 0.99;	/* a little less crazy... */
    [self EventLoopInit];

    /* if the preferences are indeed set for a CD-ROM drive, then
     * try to load a music CD, and if it's already in there go
     * to display it.  Note that this will not (yet) automatically
     * prompt to load in a CD if there isn't one there.
     */
    if ( [self openCdFd:FROM_CD_APP_DID_INIT] ) {
	[self displayCdInfo];
	}
    return self;
}

- appWillTerminate:sender
{
    /* the purpose of this method is to handle "Quit" processing */
    BOOL wantUnload, abortQuit;
    int  devReady, alertResult;
    
    if ( cd_fd == 0 ) return self;	/* no device open, nothing to do */

    /* assume an unload of the CD drive will be wanted.  If the device
     * is ready (ie, there's a disc in it), and if the disc is playing,
     * then prompt the user to find out of it's really wanted.  If the
     * drive has no CD, or the CD isn't playing, then always do an
     * unload to prevent later confusion.
     */

    wantUnload = YES;			/* assume an unload will be wanted */
    abortQuit = NO;			/* and the Quit won't be aborted */
    devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
    if ( !devReady ) {
	/* there is a disc in the drive */
	if (cd_curpos.rsc_audio_status == RSC_ASTAT_PLAYING ) {
	    alertResult = NXRunAlertPanel("mCD Quitting",
		"There is a music CD still playing.  Should it be unloaded during quit?",
		"Yes", "No", "Cancel Quit", rawDevName);
	    switch ( alertResult ) {
	        case NX_ALERTALTERNATE:
		    wantUnload = NO;
		    break;
		case NX_ALERTOTHER:
		    wantUnload = NO;
		    abortQuit = YES;
		    break;
		}
	    }
	}

    if (wantUnload) [self unloadCD:self];
    else {
	/* user wants the music CD left in the drive, playing, but
	 * we should at least enable the media-removal button (on
	 * the drive) to work again
	 */
	do_preventremoval_1e(cd_fd, NO, &cd_Ereply); /* ie, allow removal */
	}
    
    if (abortQuit) return NULL;
    
    return self;  /* anything non-null, so app will terminate */
}

- (int)appPowerOffIn:(int)ms andSave:(int)aFlag
{
    /* the purpose of this method is to handle logout processing,
     * which is not handled by the appWillTerminate method.  I
     * don't really care about power-off processing, but this
     * seems to be the only way to handle logout processing
     */

    /* this method is described *very* briefly in the pre-3.0
     * concepts documentation.  While it works in NS-3.x (on
     * motorola hardware, at least), I suspect NeXT is phasing
     * it out.  If this routine was doing any significant amount
     * of work, it might need to send the Workspace manager a:
    - (int)extendPowerOffBy:(int)requestedMs actual:(int *)actualMs;
     * message to get more time to do the processing.
     */

    /* clean up after ourselves as quickly as possible.  this is
     * basically a trimmed-down version of "unload" processing
     */
    int err;

    if ( cd_fd == 0 ) return 0;		/* no device currently, ignore */

    do_preventremoval_1e(cd_fd, NO, &cd_Ereply); /* ie, allow removal */
    do_eject_1b(cd_fd, &cd_Tval, &cd_Ereply);
    err = ioctl(cd_fd, DKIOCEJECT, NULL);
    close( cd_fd );
    cd_fd = 0;
    
    return 0 ; /* not sure what this method needs to return... */
}

- updateCdStatus:sender
{
    BOOL needUpdate, indexChanged;
    int  tempHour, tempMin, tempSec;
    int  lastCdIndex, lastAbsSecond;
    char buff[80];

    if ( cd_fd == 0 ) return self;	/* no device yet, ignore */

    needUpdate = indexChanged = NO;

    do_readcurrentposition_42(cd_fd, &cd_curpos, &rcp_Ereply);

    switch (cd_curpos.rsc_audio_status) {
	case RSC_ASTAT_PAUSED:
	case RSC_ASTAT_PLAYING:
	    break;
	case RSC_ASTAT_PLAYCOMPLETE:
	    if ( [globPrefs consoleDebugMsgs] )
		    printf("mCD debug: play complete\n");
	    [self stopCD:self];
	    break;
	case RSC_ASTAT_PLAYABORTED:
	    if ( [globPrefs consoleDebugMsgs] )
		    printf("mCD debug: play aborted\n");
	    /* presumably not wanted: [self stopCD:self]; */
	    break;
	case RSC_ASTAT_NONE:
	    break;
	}

    lastCdIndex = curCdIndex;
    curCdIndex = (cd_curpos.track * 100) + cd_curpos.index;
    if ( lastCdIndex != curCdIndex ) needUpdate = indexChanged = YES;
    
    lastAbsSecond = curAbsSecond;
    curAbsSecond =  (cd_curpos.abs_hour * 3600) + (cd_curpos.abs_min * 60) +
			    cd_curpos.abs_sec;
    if ( lastAbsSecond != curAbsSecond ) needUpdate = YES;
    
    if ( needUpdate ) {
	[mainPanel disableFlushWindow];
	
	if ( cd_curpos.index < 2 ) {
	    sprintf (buff, "%d", cd_curpos.track);
	    }
	else {
	    sprintf (buff, "%d.%d", cd_curpos.track, cd_curpos.index);
	    }
	[curTrackID setStringValue: buff];
	if ( (cd_curpos.index == 0) &&  (cd_curpos.rel_sec > 0) ) {
	    sprintf (buff, "- %02d:%02d",
			    cd_curpos.rel_min, cd_curpos.rel_sec);
	    }
	else {
	    sprintf (buff, "%d:%02d:%02d", cd_curpos.rel_hour,
			    cd_curpos.rel_min, cd_curpos.rel_sec);
	    }
	[trackPlayTimeID setStringValue: buff];
	switch (cd_curpos.rsc_audio_status) {
	  case RSC_ASTAT_PLAYING:
		/* the CD is playing right now... */
		if ( (indexChanged) &&
		     (cd_curpos.index + cd_curpos.rel_sec > 0) &&
		         [globPrefs consoleDebugMsgs] ) {
		    printf("mCD debug: index %d.%d started %d:%02d:%02d-%02d into CD",
			    cd_curpos.track, cd_curpos.index,
			    cd_curpos.abs_hour, cd_curpos.abs_min,
			    cd_curpos.abs_sec, cd_curpos.abs_frame);
		    if ((cd_curpos.rel_hour > 0) ||
		        (cd_curpos.rel_min > 0) || (cd_curpos.rel_sec > 0)) {
			if ( cd_curpos.index != 0 ) {
			    printf(", %d:%02d:%02d into the song",
				    cd_curpos.rel_hour, cd_curpos.rel_min,
				    cd_curpos.rel_sec);
			    }
			else {
			    printf(", -%02d:%02d before the song",
				    cd_curpos.rel_min, cd_curpos.rel_sec);
			    }
			}
		    printf("\n");
		    }
		tempSec = toc.info[cd_curpos.track].elapsedSec - (
			    (cd_curpos.rel_hour * 3600) +
			    (cd_curpos.rel_min * 60) +
			    cd_curpos.rel_sec );
		tempMin = tempSec / 60;
		tempSec = tempSec % 60;
		tempHour = tempMin / 60;
		tempMin = tempMin % 60;
		sprintf (buff, "%d:%02d:%02d", tempHour, tempMin, tempSec);
		[trackRemTimeID setStringValue: buff];
		tempSec = toc.info[100].elapsedSec - (
			    (cd_curpos.abs_hour * 3600) +
			    (cd_curpos.abs_min * 60) +
			    cd_curpos.abs_sec );
		tempMin = tempSec / 60;
		tempSec = tempSec % 60;
		tempHour = tempMin / 60;
		tempMin = tempMin % 60;
		sprintf (buff, "%d:%02d:%02d", tempHour, tempMin, tempSec);
		[discRemTimeID setStringValue: buff];
		break;
	}
	sprintf (buff, "%d:%02d:%02d", cd_curpos.abs_hour,
			cd_curpos.abs_min, cd_curpos.abs_sec);
	[discPlayTimeID setStringValue: buff];
	[mainPanel reenableFlushWindow];
	[mainPanel flushWindowIfNeeded];
	}
    
    return self;
}

- ejectCD:sender
{
    int  devReady;
    char buff[80];

    if ( cd_fd == 0 ) return self;	/* no device yet, ignore */
    devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
    if ( devReady )   return self;	/* device is not ready, ignore */

    do_timed_updates = NO;  /* initial testing */

    /* zap a few display fields to reduce confusion */
    [trackInfoID selectAll:self];
    [trackInfoID replaceSel: ""];

    [curTrackID setStringValue: "--"];
    [endTrackID setIntValue: toc.lasttrack];
    [trackPlayTimeID setStringValue: "-:--:--"];
    [trackRemTimeID setStringValue: "-:--:--"];
    [discPlayTimeID setStringValue: "-:--:--"];
    if( toc.info[100].min >= 60 ) {
	toc.info[100].hour++ ;
	toc.info[100].min -= 60;
	}
    sprintf (buff, "%u:%02u:%02u", toc.info[100].hour,
		    toc.info[100].min, toc.info[100].sec);
    [discRemTimeID setStringValue: buff];

    do_preventremoval_1e(cd_fd, NO, &cd_Ereply); /* ie, allow removal */
    do_eject_1b(cd_fd, &cd_Tval, &cd_Ereply);

    return self;
}

- unloadCD:sender
{
    int err;

    if ( cd_fd == 0 ) return self;	/* no device yet, ignore */
    /* (note that we can do the unload processing even if there is
     * no disc in the drive, so we don't testunitready here ) */

    [self ejectCD:self];

    /* the difference between ejecting and unloading is the
       following commands:  */
    err = ioctl(cd_fd, DKIOCEJECT, NULL);
    close( cd_fd );
    cd_fd = 0;

    [loadButtonID setEnabled:YES];
    [unloadButtonID setEnabled:NO];
    return self;
}

- loadCD:sender
{
    if ( [self openCdFd:FROM_LOAD_CD_REQUEST] ) {
	[self displayCdInfo];
	}

    return self;
}

- displayCdInfo
{
    int	track;
    char buff[120];
    NXStream *songInfoStream;

    [self updateCdStatus:self];
    switch (cd_curpos.rsc_audio_status) {
	case RSC_ASTAT_PAUSED:
	case RSC_ASTAT_PLAYING:
	    break;
	case RSC_ASTAT_PLAYCOMPLETE:
	case RSC_ASTAT_PLAYABORTED:
	case RSC_ASTAT_NONE:
	    do_spinup_1b(cd_fd, &cd_Tval, &cd_Ereply);
	}
    do_readtoc_43(cd_fd, &toc, &cd_Ereply);
    [self fillTocTitles];
    
    [curTrackID setIntValue: 0];
    [endTrackID setIntValue: toc.lasttrack];
    switch (cd_curpos.rsc_audio_status) {
	case RSC_ASTAT_PAUSED:
	    /* have to skip over this for now, due to the odd behavior
	     * of the CD-ROM drive that NeXT used to sell, which
	     * indicates it's paused if it's not actively playing and
	     * if a play operation didn't just complete.  This part
	     * could be done on a drive-specific basis, once the code
	     * is better organized.
	     */
	    /* do nothing but ensure pause button is right */
	    /* [pauseButtonID setIntValue:1]; */
	    break;
	case RSC_ASTAT_PLAYING:
	    /* do nothing but ensure pause button is right */
	    [pauseButtonID setIntValue:0];
	    break;
	case RSC_ASTAT_PLAYCOMPLETE:
	case RSC_ASTAT_PLAYABORTED:
	case RSC_ASTAT_NONE:
	    break;
    }

    [trackPlayTimeID setStringValue: "-:--:--"];
    [trackRemTimeID setStringValue: "-:--:--"];
    if( toc.info[100].min >= 60 ) {
	toc.info[100].hour++ ;
	toc.info[100].min -= 60;
	}
    sprintf (buff, "%u:%02u:%02u", toc.info[100].hour,
		    toc.info[100].min, toc.info[100].sec);
    [discRemTimeID setStringValue: buff];
    
    if ( ! toc.discTitle )
	    [cdTitleID setStringValue:"title of CD is not known"];
    else {
	    tempTitle[0] = '\0';
	    if ( toc.discPerformer ) {
		    strcat(tempTitle, toc.discPerformer);
		    strcat(tempTitle, " - ");
		    }
	    strcat(tempTitle, toc.discTitle);
	    [cdTitleID setStringValue: tempTitle];
	    }
    
    /* display the table of contents to the window, as rich-text */
    songInfoStream = NXOpenMemory(NULL, 0, NX_READWRITE);
    NXPrintf(songInfoStream,
	"{\\rtf0\\ansi{\\fonttbl\\f0\\fmodern Ohlfs;");
    NXPrintf(songInfoStream, "\\f1\\fnil Times-Roman;}\n");
    NXPrintf(songInfoStream, "\\pard\\tx460\\tx1060\\tx1260");
    NXPrintf(songInfoStream, "\\f0\\b0\\i0\\ulnone\\fs18");
    NXPrintf(songInfoStream, "\\fi-1260\\li1260\\fc0\\cf0\n");
    for ( track = toc.firsttrack; track <= toc.lasttrack; track++ ) {
	NXPrintf(songInfoStream,
		    "\\f0\\fs18%3d)\t%02d:%02d\t-\t\\f1\\fs24 ",
		    track,
		    toc.info[track].elapsedSec / 60,
		    toc.info[track].elapsedSec % 60);
	if ( toc.info[track].trackTitle )
		NXPrintf(songInfoStream, "%s", toc.info[track].trackTitle);
	else    NXPrintf(songInfoStream, "???");
	if ( track != toc.lasttrack ) NXPrintf(songInfoStream, "\\\n");
	}
    NXPrintf(songInfoStream, "\n}\n");
    NXSeek(songInfoStream, 0, NX_FROMSTART);
    [trackInfoID selectAll:self];
    [trackInfoID replaceSelWithRichText: songInfoStream];
    NXCloseMemory(songInfoStream, NX_FREEBUFFER);

    return self;
}

- pauseCD:sender
{
    /* note that this method is called from both the pause
     * button and a menu item.  Due to the menu item, this
     * has to directly set the state of the button (instead
     * of just sending a message to "sender").
     */
    do_readcurrentposition_42(cd_fd, &cd_curpos, &rcp_Ereply);
    switch (cd_curpos.rsc_audio_status) {
	case RSC_ASTAT_PLAYING:
	    /* it is playing, switch it to pause */
	    do_pauseaudio_4b(cd_fd, 1, &cd_Ereply);
	    [pauseButtonID setIntValue:1];
	    break;
	case RSC_ASTAT_PAUSED:
	    /* it is paused, switch it to playing */
	    do_pauseaudio_4b(cd_fd, 0, &cd_Ereply);
	    [pauseButtonID setIntValue:0];
	    break;
	case RSC_ASTAT_PLAYCOMPLETE:
	case RSC_ASTAT_PLAYABORTED:
	case RSC_ASTAT_NONE:
	    /* do nothing but ensure pause button is right */
	    [pauseButtonID setIntValue:0];
	    break;
    }

    return self;
}

- playCD:sender
{
    int	sTrack = 1, eTrack = toc.lasttrack;
 
    [self playTracks:sTrack to: eTrack];
    
    return self;
}

- playTrackRange: sender
{
    /* target for the play-range button in the
       select track range panel */
    int	sTrack, eTrack;

    sTrack = [newStartTrackID intValue];
    eTrack = [newEndTrackID intValue];
    [self playTracks:sTrack to: eTrack];
    [trackRangePanel performClose:self];

    return self;
}

- setLeftVolume:sender
{
    [self setPlayVolumes:[sender intValue] :-1];
    return self;
}

- setRightVolume:sender
{
    [self setPlayVolumes:-1 :[sender intValue]];
    return self;
}

- setPlayVolumes:(int) leftVol :(int) rightVol
{
    int  devReady;

    if ( cd_fd == 0 ) return self;	/* no device yet, ignore */
    devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
    if ( devReady )   return self;	/* device is not ready, ignore */
    
    if ( [globPrefs separateVolumes] || ((leftVol < 0) && (rightVol < 0))) {
	/* need to know current volume settings */
	do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
	if (leftVol < 0)  leftVol = cd_volumes.ch0_vol;
	if (rightVol < 0) rightVol = cd_volumes.ch1_vol;
	}
    else {
	if (leftVol < 0) {
	    cd_volumes.ch0_vol = leftVol = rightVol;
	    [leftVolumeSliderID setIntValue:leftVol];
	    }
	if (rightVol < 0) {
	    cd_volumes.ch1_vol = rightVol = leftVol;
	    [rightVolumeSliderID setIntValue:rightVol];
	    }
	}
    
    do_modeselect_pc_E(cd_fd, leftVol, rightVol, &cd_Ereply);
    return self;
}

/* the following used by PrefController */
- getVolumes:(int *)leftVolPtr :(int *)rightVolPtr
{
    do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
    *leftVolPtr  = cd_volumes.ch0_vol;
    *rightVolPtr = cd_volumes.ch1_vol;
    return self;
}

- showSelectTrackRange:sender
{
    [newStartTrackID setIntValue: 1];
    [newEndTrackID setIntValue: toc.lasttrack];
    [trackRangePanel makeKeyAndOrderFront:self];
    [newStartTrackID selectText:self];
    return self;
}

- goNextTrack:sender
{
    int	sTrack, eTrack;
 
    if ( cd_curpos.track >= toc.lasttrack ) {
	return self;
	}

    sTrack = cd_curpos.track + 1;
    eTrack = toc.lasttrack;  /* for now */
    [self playTracks:sTrack to: eTrack];

    return self;
}

- goPreviousTrack:sender
{
    int	sTrack, eTrack;
 
    /* goes to the beginning of the current track if we're more than
       a few seconds into it, otherwise the previous track */
    sTrack = cd_curpos.track;
    if (   (0 == cd_curpos.rel_hour) && (0 == cd_curpos.rel_min)
		&& (5 > cd_curpos.rel_sec) ) {
	sTrack = cd_curpos.track - 1;
	}
    
    if ( sTrack < 1 ) sTrack = 1;
    eTrack = toc.lasttrack;  /* for now */
    [self playTracks:sTrack to: eTrack];
    
    return self;
}

/* you might ask, why does this go thru all the trouble of
 * finding starting and ending times and doing a playaudio_msf
 * instead of just playing a track range?
 *
 * The answer is that I want flexibility such that the CD database
 * can *change* the start and end times for a track, for those
 * CD's which were manufactured with the wrong starting times.
*/
- playTracks:(int)startTrack to: (int) endTrack
{
    struct pa_msf tst_start = {0, 0, 0};
    struct pa_msf tst_end = {0, 0, 0};
    int aeTrack;
    int  devReady;
    
    if ( cd_fd == 0 ) return self;	/* no device currently, ignore */
    devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
    if ( devReady )   return self;	/* device is not ready, ignore */
    
    /* calc the track after the end track*/
    if ( endTrack >= toc.lasttrack ) aeTrack = 100;
    else aeTrack = endTrack + 1;
    
    /* note that do_playaudio_msf_47 has no hours variable... */
    tst_start.min = (toc.info[startTrack].hour * 60)
    			+ toc.info[startTrack].min;
    tst_start.sec = toc.info[startTrack].sec;
    tst_start.frame = toc.info[startTrack].frame;

    tst_end.min = (toc.info[aeTrack].hour * 60) + toc.info[aeTrack].min;
    tst_end.sec = toc.info[aeTrack].sec;
    tst_end.frame = toc.info[aeTrack].frame;
    if ( tst_end.frame > 0 ) tst_end.frame--;
    else {
	tst_end.frame = 74;
        if ( tst_end.sec > 0 ) tst_end.sec--;
	else {
	    tst_end.sec = 59;
	    if ( tst_end.min > 0 ) tst_end.min--;
	    }
	}
    
    if ( [globPrefs consoleDebugMsgs] ) {
	printf("mCD debug: start (%d) %02u:%02u-%02u",
		    startTrack,
		    tst_start.min, tst_start.sec, tst_start.frame);
	printf("  end (%d) %02u:%02u-%02u\n",
		    endTrack,
		    tst_end.min, tst_end.sec, tst_end.frame);
	}
    do_playaudio_msf_47(cd_fd, tst_start, tst_end, &cd_Ereply);

    /* make sure the volumes are set right (doing a playaudio command
     * causes the CD to play at full volume, even if modesense shows
     * that some other volume level has been set)
     */
    do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
    [leftVolumeSliderID setIntValue: cd_volumes.ch0_vol];
    [rightVolumeSliderID setIntValue: cd_volumes.ch1_vol];
    [self setPlayVolumes:cd_volumes.ch0_vol :cd_volumes.ch1_vol];

    /* ensure pause button is set right */
    [pauseButtonID setIntValue:0];

    return self;
}

- showInfoPanel:sender
{
    if ( ! _mCD_InfoPanel )
	_mCD_InfoPanel = [NXApp loadNibSection:"mCD_Info.nib" owner:self];
    [_mCD_InfoPanel makeKeyAndOrderFront:self];

    return self;
}

- showTestCD:sender
{
    if ( ! testCD_ID )
	[NXApp loadNibSection:"testCD.nib" owner:self];
    [testCD_ID showUsingPrefs:globPrefs];

    return self;
}

- stopCD:sender
{
    int  devReady;
    char buff[40];

    if ( cd_fd == 0 ) return self;	/* no device yet, ignore */
    devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
    if ( devReady )   return self;	/* device is not ready, ignore */
    
    do_rezerounit_01(cd_fd, &cd_Ereply);
    do_stopunit_1b(cd_fd, &cd_Tval, &cd_Ereply);
    curCdIndex = -1;
    curAbsSecond = -1;

    /* probably should reset time-displaying fields differently */
    [trackRemTimeID setStringValue: "-:--:--"];
    if( toc.info[100].min >= 60 ) {
	toc.info[100].hour++ ;
	toc.info[100].min -= 60;
	}
    sprintf (buff, "%u:%02u:%02u", toc.info[100].hour,
		    toc.info[100].min, toc.info[100].sec);
    [discRemTimeID setStringValue: buff];

    return self;
}

- copyMcdEntry:sender
{
    int	 track;
    NXStream  *mstream;
    id   pboard;
    NXAtom ptypes[1];

    /* note: probably should check that there really is an mCD entry
     *       to copy at this point in time...
     */
    mstream = NXOpenMemory(NULL, 0, NX_READWRITE);
    if( toc.info[100].min >= 60 ) {
	toc.info[100].hour++ ;
	toc.info[100].min -= 60;
	}
    NXPrintf(mstream, "%u:%02u:%02u\t", toc.info[100].hour,
		       toc.info[100].min, toc.info[100].sec);
	
    if ( ! toc.discTitle )
	NXPrintf(mstream, "**artist unknown**\t- ** title unknown **");
    else {
	if ( ! toc.discPerformer )
	    NXPrintf(mstream, "**artist unknown**");
	else
	    NXPrintf(mstream, "%s", toc.discPerformer);
	NXPrintf(mstream, "\t- %s", toc.discTitle);
	}
    if ( toc.discCatNum )
	NXPrintf(mstream, "\tUPC=%s", toc.discCatNum);
    NXPrintf(mstream, "\n");
	
    /* include the table of contents */
    for ( track = toc.firsttrack; track <= toc.lasttrack; track++ ) {
	NXPrintf(mstream, "%4d)\t%02d:%02d\t- ", track,
			  toc.info[track].elapsedSec / 60,
			  toc.info[track].elapsedSec % 60);
	if ( toc.info[track].trackTitle )
		NXPrintf(mstream, "%s", toc.info[track].trackTitle);
	else    NXPrintf(mstream, "???");
	NXPrintf(mstream, "\n");
	}
    NXPrintf(mstream, "\n");

    pboard = [Pasteboard new];
    ptypes[0] = NXAsciiPboardType;
    [pboard declareTypes:ptypes num:1 owner:self];
    [pboard writeType:NXAsciiPboardType fromStream:mstream];
    NXCloseMemory(mstream, NX_FREEBUFFER);
    
    return self;
}

- copyMcdEntryAsObjC:sender
{
    u_int key;
    int	 track;
    NXStream  *mstream;
    id   pboard;
    NXAtom ptypes[1];

    /* note: probably should check that there really is an mCD entry
     *       to copy at this point in time...
     */
    mstream = NXOpenMemory(NULL, 0, NX_READWRITE);
    key = [cd_dbase indexKey:&toc];
    NXPrintf(mstream, "#define %s %10u /* key parts = %d.%02u.%02d.%d %d */\n",
             "0000_none_0000", key, toc.info[100].elapsedSec,
	     toc.info[100].frame, toc.lasttrack, toc.info[1].elapsedSec,
	     toc.info[toc.lasttrack].elapsedSec);

    if( toc.info[100].min >= 60 ) {
	toc.info[100].hour++ ;
	toc.info[100].min -= 60;
	}
    NXPrintf(mstream, "    if ( cdKey == %s ) { /* %u:%02u:%02u-%02u */\n",
             "0000_none_0000", toc.info[100].hour,
		       toc.info[100].min, toc.info[100].sec, toc.info[100].frame);
	
    if ( ! toc.discPerformer )
	NXPrintf(mstream, "\ttocPtr->discPerformer = \"performer??\";\n");
    else
	NXPrintf(mstream, "\ttocPtr->discPerformer = \"%s\";\n", toc.discPerformer);

    if ( ! toc.discTitle )
	NXPrintf(mstream, "\ttocPtr->discTitle = \"title??\";\n");
    else
	NXPrintf(mstream, "\ttocPtr->discTitle = \"%s\";\n", toc.discTitle);

    if ( toc.discCatNum )
	NXPrintf(mstream, "\ttocPtr->discCatNum = \"%s\";\n", toc.discCatNum);

#ifdef ADD_SKIPSONGS
    NXPrintf(mstream, "#     if !defined(SKIPSONGS_\n");
#endif
    
    /* include the table of contents */
    for ( track = toc.firsttrack; track <= toc.lasttrack; track++ ) {
	NXPrintf(mstream, "\tSET_CDtt(%2d, %02d.%02d, \"", track,
			  toc.info[track].elapsedSec / 60,
			  toc.info[track].elapsedSec % 60);
	if ( toc.info[track].trackTitle )
		NXPrintf(mstream, "%s", toc.info[track].trackTitle);
	NXPrintf(mstream, "\");\n");
	}

    /* finish off the entry */
#ifdef ADD_SKIPSONGS
    NXPrintf(mstream, "#     endif\n");
#endif
    NXPrintf(mstream, "\t}\n");

    pboard = [Pasteboard new];
    ptypes[0] = NXAsciiPboardType;
    [pboard declareTypes:ptypes num:1 owner:self];
    [pboard writeType:NXAsciiPboardType fromStream:mstream];
    NXCloseMemory(mstream, NX_FREEBUFFER);
    
    return self;
}

- pasteMcdEntry:sender
{
    return self;
}

- (BOOL)openCdFd:(BOOL)tryToLoad
{
    int	devReady;

    strcpy(rawDevName, [globPrefs rawDeviceName]);

    if ( cd_fd > 0 ) close( cd_fd );

    if (tryToLoad == FROM_LOAD_CD_REQUEST) {
	cd_fd = open( rawDevName, O_RDONLY );
	if ( cd_fd < 0 ) {
	    NXRunAlertPanel(0,
		"Error return from open() for device %s",
		0, 0, 0, rawDevName);
	    cd_fd = 0;
	    return NO;
	    }
	}
    else {
	cd_fd = open( rawDevName, O_RDONLY | O_NDELAY );
	if ( cd_fd < 0 ) {
	    cd_fd = 0;
	    return NO;
	    }
	}

    /* should check the result of do_inquiry to see which model
     * of CD-ROM drive is there (so this would know which SCSI
     * commands can be sent to the drive) */
    do_inquiry(cd_fd, &cd_Inq, &cd_Ereply);
    
    if ( cd_Inq.ir_devicetype != DEVTYPE_CDROM ) {
	if (tryToLoad == FROM_LOAD_CD_REQUEST) {
	    NXRunAlertPanel(0,
		"Device %s does not seem to be a CD-ROM drive",
		0, 0, 0, rawDevName);
	    }
	else {
	    printf("Device %s does not seem to be a CD-ROM drive\n",
	     	rawDevName);
	    }
	close( cd_fd );
	return NO;
	}

    devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
    if ( cd_Ereply.er_sensekey == SENSE_UNITATTENTION ) {
	/* drive is signalling attention because there is a new disc
	 * in the drive since the last time it was checked.  In this
	 * context (loading in a CD), we don't care that it's new
	*/
	devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
	}
    if (    (tryToLoad == FROM_LOAD_CD_REQUEST)
         && (devReady == -1)
	 && (cd_Ereply.er_sensekey == SENSE_NOSENSE) ) {
	/* I don't understand what the culprit is that forces this
	 * special case.  It is needed on my NS/Intel box, but only
	 * on the *first* time the "Load" button is selected after
	 * the system is rebooted.  For some reason, "loads" at that
	 * point will not succeed unless an unload is first done on
	 * the device... 
	*/
	int err;
	printf("mCD; device not ready, unloading and reloading the CD\n");
	err = ioctl(cd_fd, DKIOCEJECT, NULL);
	close( cd_fd );
	cd_fd = 0;
	cd_fd = open( rawDevName, O_RDONLY );
	if ( cd_fd < 0 ) {
	    NXRunAlertPanel(0,
		"Error return from second open() on device %s",
		0, 0, 0, rawDevName);
	    cd_fd = 0;
	    return NO;
	    }
	devReady = do_testunitready(cd_fd, &cd_Tval, &tur_Ereply);
	}
    if ( devReady ) {
	if (tryToLoad == FROM_LOAD_CD_REQUEST) {
	    printf("mCD: Device %s is not ready (%d, %d)\n", rawDevName,
		    devReady, cd_Ereply.er_sensekey);
	    /* the cd_fd is not freed here, on purpose */
	    /* -------
	     * Hmm.  I think I'll change my mind on that.  I forget
	     * why I didn't want to free it, but now I think I do
	     * at least for one case.
	     */
	    if ((devReady == -1) && (cd_Ereply.er_sensekey == SENSE_NOSENSE)) {
		int err;
		printf("mCD; device not ready, unloading the CD\n");
		err = ioctl(cd_fd, DKIOCEJECT, NULL);
		close( cd_fd );
		cd_fd = 0;
		}
	    }
	else {
	    /* freeing cd_fd in this case might be a preference option
	     * to add someday... */
	    close( cd_fd );
	    }
	return NO;
	}

    if (tryToLoad == FROM_CD_APP_DID_INIT) {
    /* need to think more about under what circumstances the current
     * volumes should be set from global preference values
     */
	int leftInt, rightInt;
	[globPrefs getVolumes:&leftInt:&rightInt];
	cd_volumes.ch0_vol = leftInt;
	cd_volumes.ch1_vol = rightInt;
	[self setPlayVolumes:cd_volumes.ch0_vol :cd_volumes.ch1_vol];
    } else {
	do_modesense_pc_E(cd_fd, &cd_volumes, &cd_Ereply);
    }
    [leftVolumeSliderID setIntValue: cd_volumes.ch0_vol];
    [rightVolumeSliderID setIntValue: cd_volumes.ch1_vol];

    curCdIndex = -1;
    curAbsSecond = -1;

    /* do_preventremoval_1e(cd_fd, YES, &cd_Ereply); */
    do_timed_updates = YES;  /* initial testing */
    [loadButtonID setEnabled:NO];
    [unloadButtonID setEnabled:YES];
    return YES;		/* successfully setup */
}

- fillTocTitles
{
    u_int	key;
    struct rsc_media_catnum_reply cd_catnum;
    char        origCatNumber[MEDIA_CATNUM_LENGTH];
    BOOL        switchUPC;

    key = [cd_dbase indexKey:&toc];

    setreuid(-1, geteuid());			/* switch to real uid */
    [cd_dbase fillTocTitles:&toc givenKey:key];
    setreuid(-1, 0);				/* back to being root */
 
    /* check out the media catalog number, if this CD has one */
    switch (cd_curpos.rsc_audio_status) {
	case RSC_ASTAT_PLAYING:
	    /* don't read the media catalog number.  Trying to read
	     * it will stop the CD on the Apple CD300 and NeXT
	     * drives.  [on the Toshiba drive, it doesn't stop the
	     * music, and in fact the command sometimes won't work
	     * return the catalog number unless the CD *is* playing!]
	     */
	    if ( !toc.discCatNum && [globPrefs consoleDebugMsgs] ) {
		printf("mCD info: didn't check for media catalog number\n");
		}
	    break;
	case RSC_ASTAT_PAUSED:
	    /* it'd be nice to take the same precaution when the cd-rom
	     * drive is paused, but it seems that the CD-ROM drive that
	     * NeXT used to sell (a Sony mechanism) returns "paused" at
	     * times when it's not really paused.  The Apple CD300 (a
	     * newer Sony mechanism) doesn't seem to have this problem.
	     */
	case RSC_ASTAT_PLAYCOMPLETE:
	case RSC_ASTAT_PLAYABORTED:
	case RSC_ASTAT_NONE:
	    switchUPC = NO;
	    do_readmediacatnum_42(cd_fd, &cd_catnum, &cd_Ereply);
	    if ( cd_catnum.catnum_isSet ) {
		memcpy(origCatNumber, cd_catnum.media_catnum, MEDIA_CATNUM_LENGTH);
		fixCatNum(&cd_catnum);
		if (!toc.discCatNum) switchUPC = YES;
		else {
		    if ( strncmp(toc.discCatNum, cd_catnum.media_catnum, MEDIA_CATNUM_LENGTH)) {
			/* the UPC in our database does not match the UPC this
			 * drive found on the CD itself.  First have to check
			 * for lame NeXT-CD-ROM case (which misses 3 chars)
			 */
			if ( cd_catnum.media_catnum[MEDIA_CATNUM_LENGTH-1] != '?') switchUPC = YES;
			else {
			    if ( strncmp(toc.discCatNum,cd_catnum.media_catnum,
					    MEDIA_CATNUM_LENGTH-3)) switchUPC = YES;
			    }
			}
		    if ( switchUPC && [globPrefs consoleDebugMsgs]) {
			/* tell the user of mismatch */
			int	i;
			u_char	*cptr; 
			printf("mCD info: original UPC = x'");
			cptr = &(origCatNumber[0]);
			printf("%.2X", *cptr++);  /* ignore comp. warning */
			for (i=1; i<MEDIA_CATNUM_LENGTH; i++) {
			    printf(" %.2X", *cptr++);  /* ignore comp. warning */
			    }
			printf("'\n");
			printf("mCD info: fixed up UPC =  \"%.15s\"\n", cd_catnum.media_catnum);
			printf("mCD info: DBase return =  \"%15s\"\n", toc.discCatNum);
			printf("mCD info: currently using the fixed upCD's catalog number\n");
			}
		    }
		}
	    if ( switchUPC ) {
		memcpy(holdCatNumber, cd_catnum.media_catnum, MEDIA_CATNUM_LENGTH);
		holdCatNumber[MEDIA_CATNUM_LENGTH+1] = '\0';
		toc.discCatNum = &(holdCatNumber[0]);
		}
	    /* Redoing the "spinup" here seems to reduce the problem
	     * of getting a unit attn when do_readmediacatnum finds
	     * a media code on the music CD */
	    do_spinup_1b(cd_fd, &cd_Tval, &cd_Ereply);
	    break;
	}

    return self;
}


/* these next few are for the benefit of the testCD module,
 * to implement the protocol testCDstarter */
- (int) fdOfCdrom  { return cd_fd; }
- (struct esense_reply*) esenseReplyPtr { return &cd_Ereply; }
- (struct esense_reply*) esenseTestUnitPtr { return &tur_Ereply; }
- (struct esense_reply*) esenseCurPosPtr { return &rcp_Ereply; }

@end

/* I've been testing this program on three different models of CD-ROM
 * drives.  Each one returns the catalog number in a different format.
 * In the case of the CD-ROM drive NeXT used to sell, it doesn't even
 * return the whole number.  Do the best we can to map the three
 * formats (that I know of so far) into a common format.
 *
 *   Apple CD300 UPC = '3030 3735 3939 3235 3739 3432 3130 30'
 *   DEC-Toshiba UPC = '0000 0705 0909 0205 0709 0402 0100 00'
 *   NeXT CD-ROM UPC = '0075 9925 7942 0000 0000 0000 0000 00'
 *
 * Not sure if this routine should really be in the scsi_cd subproj...
 */
void fixCatNum(catNumPtr)
struct rsc_media_catnum_reply *catNumPtr;
{
    char *tempPtr, *endPtr;
    BOOL all_char, all_dec;

    all_char = YES; /* all bytes are characters '0' to '9' */
    all_dec = YES;  /* all bytes are 0x00 to 0x09 */
    tempPtr = &(catNumPtr->media_catnum[0]);
    endPtr = tempPtr + MEDIA_CATNUM_LENGTH;
    for (; tempPtr < endPtr; tempPtr++) {
	if (*tempPtr < '0' || *tempPtr > '9') all_char = NO;
	if (*tempPtr > 0x09) all_dec = NO;
    }
    if (all_dec) {
	/* all decimal, change them to all-characters */
	tempPtr = catNumPtr->media_catnum;
	for (; tempPtr < endPtr; tempPtr++) *tempPtr += 0x30;
	all_dec = NO;
	all_char = YES;
    }
    if (all_char) return;
    
    /* the NeXT format, have to expand it, going right-to-left */
    /* [done the brute-force way, and assumes MEDIA_CATNUM_LENGTH = 15] */
    catNumPtr->media_catnum[14] = (catNumPtr->media_catnum[7] >> 4) + 0x30;
    catNumPtr->media_catnum[13] = (catNumPtr->media_catnum[6] & 0x0F) + 0x30;
    catNumPtr->media_catnum[12] = (catNumPtr->media_catnum[6] >> 4) + 0x30;
    catNumPtr->media_catnum[11] = (catNumPtr->media_catnum[5] & 0x0F) + 0x30;
    catNumPtr->media_catnum[10] = (catNumPtr->media_catnum[5] >> 4) + 0x30;
    catNumPtr->media_catnum[9] = (catNumPtr->media_catnum[4] & 0x0F) + 0x30;
    catNumPtr->media_catnum[8] = (catNumPtr->media_catnum[4] >> 4) + 0x30;
    catNumPtr->media_catnum[7] = (catNumPtr->media_catnum[3] & 0x0F) + 0x30;
    catNumPtr->media_catnum[6] = (catNumPtr->media_catnum[3] >> 4) + 0x30;
    catNumPtr->media_catnum[5] = (catNumPtr->media_catnum[2] & 0x0F) + 0x30;
    catNumPtr->media_catnum[4] = (catNumPtr->media_catnum[2] >> 4) + 0x30;
    catNumPtr->media_catnum[3] = (catNumPtr->media_catnum[1] & 0x0F) + 0x30;
    catNumPtr->media_catnum[2] = (catNumPtr->media_catnum[1] >> 4) + 0x30;
    catNumPtr->media_catnum[1] = (catNumPtr->media_catnum[0] & 0x0F) + 0x30;
    catNumPtr->media_catnum[0] = (catNumPtr->media_catnum[0] >> 4) + 0x30;
    /* and after all that, we probably have three zeros at the end,
     * which most-likely means we've lost the last three digits.
     * Put in question-marks, I guess.
     */
   if (   (catNumPtr->media_catnum[14] == '0')
       && (catNumPtr->media_catnum[13] == '0')
       && (catNumPtr->media_catnum[12] == '0') ) {
       catNumPtr->media_catnum[12] = '?';
       catNumPtr->media_catnum[13] = '?';
       catNumPtr->media_catnum[14] = '?';
   }
}
